package cn.roylion.resilience4j.core;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.RemovalListener;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.internal.AtomicRateLimiter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.cloud.gateway.filter.ratelimit.AbstractRateLimiter;
import org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator;
import org.springframework.cloud.gateway.support.ConfigurationService;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 * @author genxin.liu
 * @since 2022/7/22 13:14
 */
public class Resilience4JRateLimiter extends AbstractRateLimiter<Resilience4JRateLimiter.Config> {

    public static final String CONFIGURATION_PROPERTY_NAME = "resilience4j-rate-limiter";

    private static final Log log = LogFactory.getLog(Resilience4JRateLimiter.class);
    public static final int LIMIT_FOR_PERIOD = 50;
    public static final String LIMIT_REFRESH_PERIOD = "500N";
    public static final String TIMEOUT_DURATION = "5S";

    private Resilience4JRateLimiterProperties properties;

    private Config defaultConfig = new Config(LIMIT_FOR_PERIOD, LIMIT_REFRESH_PERIOD, TIMEOUT_DURATION);

    /**
     * key = routeId + id
     * value = RateLimiter
     */
    private Cache<String, RateLimiter> rateLimiterCache;

    public Resilience4JRateLimiter(Cache<String, RateLimiter> rateLimiterCache, Resilience4JRateLimiterProperties properties, ConfigurationService configurationService) {
        super(Config.class, CONFIGURATION_PROPERTY_NAME, configurationService);
        this.properties = properties;
        this.rateLimiterCache = rateLimiterCache;
    }

    public Resilience4JRateLimiter(Resilience4JRateLimiterProperties properties, ConfigurationService configurationService) {
        super(Config.class, CONFIGURATION_PROPERTY_NAME, configurationService);
        this.properties = properties;
        Resilience4JRateLimiterProperties.CacheConfig cacheConfig = properties.getCache();
        this.rateLimiterCache = Caffeine.newBuilder()
                .initialCapacity(cacheConfig.getInitialCapacity())
                .maximumSize(cacheConfig.getMaximumSize())
                .expireAfterAccess(Duration.ofMinutes(cacheConfig.getExpireAfterAccess()))
                .removalListener(new EvictionListener())
                .build();
    }

    public static void main(String[] args) {
        Resilience4JRateLimiterProperties.CacheConfig cacheConfig = new Resilience4JRateLimiterProperties.CacheConfig();
        int initialCapacity = Optional.ofNullable(cacheConfig).map(Resilience4JRateLimiterProperties.CacheConfig::getInitialCapacity).orElse(128);
        System.out.println(initialCapacity);
    }

    /**
     * @param routeId 配置
     * @param id      用户
     * @return
     */
    @Override
    public Mono<Response> isAllowed(String routeId, String id) {
        RateLimiter rateLimiter = getRateLimiter(routeId, id);
        boolean allow = rateLimiter.acquirePermission();
        Response response = new Response(allow, getHeaders(rateLimiter));
        if (log.isDebugEnabled()) {
            log.debug("response:" + response);
        }
        return Mono.just(response);
    }

    private Map<String, String> getHeaders(RateLimiter rateLimiter) {
        HashMap<String, String> headers = new HashMap<>();
        RateLimiterConfig rateLimiterConfig = rateLimiter.getRateLimiterConfig();
        headers.put("X-RateLimit-Limit", Integer.toString(rateLimiterConfig.getLimitForPeriod()));
        headers.put("X-RateLimit-Period", String.valueOf(rateLimiterConfig.getLimitRefreshPeriod()));
        headers.put("X-RateLimit-Timeout", String.valueOf(rateLimiterConfig.getTimeoutDuration()));
        RateLimiter.Metrics metrics = rateLimiter.getMetrics();
        headers.put("X-RateLimit-Available-Permissions", Integer.toString(metrics.getAvailablePermissions()));
        headers.put("X-RateLimit-Waiting-Threads", Integer.toString(metrics.getNumberOfWaitingThreads()));
        return headers;
    }

    RateLimiter getRateLimiter(String routeId, String id) {
        String key = routeId + "_" + id;

        RateLimiter cache = rateLimiterCache.getIfPresent(key);
        if (Objects.nonNull(cache)) {
            return cache;
        }
        synchronized (rateLimiterCache) {
            return rateLimiterCache.get(key, k -> buildRateLimiter(key, routeId, id));
        }
    }

    RateLimiter buildRateLimiter(String key, String routeId, String id) {
        Config config = loadConfiguration(routeId, id);
        Integer limitForPeriod = Objects.nonNull(config.getLimitForPeriod()) ? config.getLimitForPeriod() : LIMIT_FOR_PERIOD;
        String limitRefreshPeriod = StringUtils.hasLength(config.getLimitRefreshPeriod()) ? config.getLimitRefreshPeriod() : LIMIT_REFRESH_PERIOD;
        String timeoutDuration = StringUtils.hasLength(config.getTimeoutDuration()) ? config.getTimeoutDuration() : TIMEOUT_DURATION;

        RateLimiterConfig rateLimiterConfig = RateLimiterConfig.custom()
                .limitForPeriod(limitForPeriod)
                .limitRefreshPeriod(parse("limitRefreshPeriod", limitRefreshPeriod))
                .timeoutDuration(parse("timeoutDuration", timeoutDuration))
                .build();
        // 默认为令牌桶限流器
        RateLimiter rateLimiter = new AtomicRateLimiter(key, rateLimiterConfig);
        return rateLimiter;
    }

    Config loadConfiguration(String routeId, String id) {
        String originRouteId = routeId;
        Resilience4JRateLimiter.Config routeConfig = getConfig().get(routeId);

        if (routeId.startsWith("ReactiveCompositeDiscoveryClient_")) {
            routeId = routeId.substring("ReactiveCompositeDiscoveryClient_".length());
        }
        if (routeConfig == null) {
            routeConfig = properties.getConfigs().get(routeId + "." + id);
        }

        if (routeConfig == null) {
            routeConfig = properties.getConfigs().get(routeId);
        }

        if (routeConfig == null) {
            routeConfig = getConfig().get(RouteDefinitionRouteLocator.DEFAULT_FILTERS);
        }

        if (routeConfig == null) {
            return defaultConfig;
//            throw new IllegalArgumentException("No Configuration found for route " + originRouteId + " or defaultFilters");
        }
        return routeConfig;
    }

    @Validated
    public static class Config {

        /**
         * 限流数, 默认50
         */
        private Integer limitForPeriod;

        /**
         * 限流周期, 默认500纳秒
         * nD => n 天
         * nH => n 小时
         * nM => n 分钟
         * nS => n 秒
         * nm => n 毫秒
         * nN => n 纳秒
         *
         * @see
         */
        private String limitRefreshPeriod;

        /**
         * 等待超时时间, 默认5秒
         * nD => n 天
         * nH => n 小时
         * nM => n 分钟
         * nS => n 秒
         * nm => n 毫秒
         * nN => n 纳秒
         *
         * @see Duration#parse(CharSequence)
         */
        private String timeoutDuration;

        public Config() {
        }

        public Config(Integer limitForPeriod, String limitRefreshPeriod, String timeoutDuration) {
            this.limitForPeriod = limitForPeriod;
            this.limitRefreshPeriod = limitRefreshPeriod;
            this.timeoutDuration = timeoutDuration;
        }

        public Integer getLimitForPeriod() {
            return this.limitForPeriod;
        }

        public void setLimitForPeriod(Integer limitForPeriod) {
            this.limitForPeriod = limitForPeriod;
        }

        public String getLimitRefreshPeriod() {
            return this.limitRefreshPeriod;
        }

        public void setLimitRefreshPeriod(String limitRefreshPeriod) {
            this.limitRefreshPeriod = limitRefreshPeriod;
        }

        public String getTimeoutDuration() {
            return this.timeoutDuration;
        }

        public void setTimeoutDuration(String timeoutDuration) {
            this.timeoutDuration = timeoutDuration;
        }
    }

    public static class EvictionListener implements RemovalListener<String, RateLimiter> {
        @Override
        public void onRemoval(@Nullable String key, @Nullable RateLimiter value, @NonNull RemovalCause cause) {
            log.info(String.format("%s eviction. %s", key, cause.name()));
        }
    }

    private Duration parse(String key, String arg) {
        if (!StringUtils.hasLength(arg)) {
            throw new IllegalArgumentException(key + " is blank.");
        }

        long t;
        try {
            String time = arg.substring(0, arg.length() - 1);
            t = Long.valueOf(time);
        } catch (Exception e) {
            throw new IllegalArgumentException(key + " is not invalid, val : " + arg);
        }
        String suffix = arg.substring(arg.length() - 1);
        switch (suffix) {
            case "D":
                return Duration.ofDays(t);
            case "H":
                return Duration.ofHours(t);
            case "M":
                return Duration.ofMinutes(t);
            case "S":
                return Duration.ofSeconds(t);
            case "m":
                return Duration.ofMillis(t);
            case "N":
                return Duration.ofNanos(t);
            default:
                throw new IllegalArgumentException(key + " is not invalid, val : " + arg);
        }
    }

}
